<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

/**
 * Paginator Class
 *
 * This class is based heavily on the CI 2.0 core Pagination Class.
 * However, if you pass in the right parameters, it will hopefully work some magic
 * for you and create a table with sortable headers, as well as selectable page sizes
 *
 * Although the core Pagination class works off the offset param, this class uses the the
 * page number and page size, as well as the sort order and sort direction.
 * For this to happen, the uri has to be constructed in a very specific order of:
 * /page_no/page_size/sort_by/sort_order. So, for example, products/index/2/25/code/desc
 * means that I want to show the 2nd page, 25 records, sorted by code, sorted in descending order
 *
 * Note that this class does not yet work with query strings
 *
 * @license		DBAD (http://philsturgeon.co.uk/code/dbad-license)
 * @author		JonoB
 *
 * @file		Paginator.php
 * @version		0.1
 * @date		02/23/2011

 */

/*
// --------------------------------------------------------------------
1. Getting started:

1.1 Drop this file into your application/libraries/ folder
1.2 Initialise it in a controller by using $this->load->library('paginator');
1.3 Pass in some parameters (see below)
1.4 There are 4 sections to output:

1.4.1 To create your standard pagination links ala the CI Core Pagination Lib.
$this->paginator->create_links();

Important: the create_links() function must be called before any of the following
functions, otherwise they will not work

1.4.2 To create the actual table with all your records, and sortable headers:
$this->paginator->create_table();

1.4.3 To create selectable page sizes.
$this->paginator->create_page_sizes();
This is based on the $page_sizes var. By default, this is set to (10, 25, 50, 100)
but you can pass in a config array of any values that you want.

1.4.4 To create a summary
$this->paginator->create_summary();
This outputs an array, so that you can create some verbose output on the current recordset, like:
Page 1 of 2, showing 20 records out of 27 total, starting on record 1, ending on 20


// --------------------------------------------------------------------

2. Parameters
I have tried to make it as easy to use for my scenario, which means
that a lot of the params have default values, and I pass in only the minimum number required

In order for all of the above to work properly, at a minimum the the following parameters must
be available.

2.1 table_columns
This is an array of data that you want shown in the table, and it also gives
instructions on how you want those columns treated. See the example controller
section below for more information.

2.2 model_name
You must pass in a valid model name. Example: Products_model

2.3 count_function
This is the function name in the above model that counts all your records.
Dont include the opening and closing brackets after the function name
Example: count

2.4 record_function
This is the function name in the above model that returns your records.
Dont include the opening and closing brackets after the function name
Example: get_products
Important: The parameters for this function MUST be in this order: $sort_by, $sort_order, $offset, $limit
Important: This function must return (at a minimum) all columns that are identified as 'sortable' in $config['table_columns'] (see 2.1 above)

2.5 base_url
This is usually the controller/function that you are working from. 
Example: site_url('products/index/');

// --------------------------------------------------------------------

3. Example Controller:

$this->load->library('paginator');

$columns = array(
    array(
        'database' => 'code', //field name in the database
        'header' => 'Code', //text for the table heading
        'sortable' => TRUE, //Allow sorting on this column?
        'default_sort' => TRUE, //is this the default sort column? One column must have this property set to true
        'anchor_link' => 'products/edit/', //if set, then the data in the rows will link to this controller/function
        'anchor_field' => 'id' //and it will link to this field in the database
    ),
    array(
        'database' => 'description',
        'header' => 'Description',
        'sortable' => TRUE
    ),
    array(
        'header' => 'Actions',
        'sortable' => FALSE, //note that this column is not sortable
        'text' => 'Delete',
        'anchor_field' => 'id',
        'anchor_link' => 'products/delete/',
        'extra' => array(
                'class' => 'delete',
                'onclick' => "return confirm('Are you sure want to delete this product?')")
    )
 );

//pagination
$config['table_columns']    = $columns;
$config['base_url']         = site_url('products/index/');
$config['model_name']       = 'Product_model';
$config['record_function']  = 'get_products';
//$config['count_function'] = 'count'; //note that I dont set this param, because the default
param for this variable in the class is 'count', and I make sure that all the relevant models have this function name

$this->paginator->initialize($config);

$data['pagination'] = $this->paginator->create_links(); //note that this must be called before any of the following
$data['table']      = $this->paginator->create_table();
$data['page_sizes'] = $this->paginator->create_page_sizes();
$data['summary']    = $this->paginator->create_summary();

$this->load->view('products/index', $data);
*/

class CI_Paginator {

	var $base_url               = ''; // The page we are linking to
	var $prefix                 = ''; // A custom prefix added to the path.
	var $suffix                 = ''; // A custom suffix added to the path.
	var $total_rows             = ''; // Total number of items (database results)
	var $page_size              = 25; // Number of items you want shown per page by default
    var $num_links              =  2; // Number of "digit" links to show before/after the currently viewed page
	var $cur_page               =  0; // The current page being viewed
	var $first_link             = '&laquo First';
	var $next_link              = 'Next &rsaquo;';
	var $prev_link              = '&lsaquo; Previous';
	var $last_link              = 'Last &raquo;';
	var $uri_segment            = 3;
	var $full_tag_open          = '<ul>';
	var $full_tag_close         = '</ul>';
	var $first_tag_open         = '<li>';
	var $first_tag_close        = '</li>';
	var $last_tag_open          = '<li>';
	var $last_tag_close         = '</li>';
	var $first_url              = ''; // Alternative URL for the First Page.
	var $cur_tag_open           = '<li class="selected">';
	var $cur_tag_close          = '</li>';
	var $next_tag_open          = '<li>';
	var $next_tag_close         = '</li>';
	var $prev_tag_open          = '<li>';
	var $prev_tag_close         = '</li>';
	var $num_tag_open           = '<li>';
	var $num_tag_close          = '</li>';
    var $page_tag_open          = '<ul>';
    var $page_tag_close         = '</ul>';

	var $page_query_string      = FALSE;
	var $query_string_segment   = 'page_size';
	var $display_pages          = TRUE;
	var $anchor_class           = '';

    var $page_sizes             = array(10, 25, 50, 100);
    var $size_tag_open          = '<ul>';
    var $size_tag_close         = '</ul>';

    var $table_columns          = '';
    var $model_name             = '';
    var $count_function         = 'count';
    var $record_function        = '';

    private $_table_records;
    private $_num_pages         = ''; //total number of pages
    private $CI                 = '';

	/**
	 * Constructor
	 *
	 * @access	public
	 * @param	array	initialization parameters
	 */
	public function __construct($params = array())
	{
		if (count($params) > 0)
		{
			$this->initialize($params);
		}

		if ($this->anchor_class != '')
		{
			$this->anchor_class = 'class="'.$this->anchor_class.'" ';
		}

        $this->CI =& get_instance();

        log_message('debug', "Pagination Class Initialized");
	}

	// --------------------------------------------------------------------

	/**
	 * Initialize Preferences
	 *
	 * @access	public
	 * @param	array	initialization parameters
	 * @return	void
	 */
	function initialize($params = array())
	{
		if (count($params) > 0)
		{
			foreach ($params as $key => $val)
			{
				if (isset($this->$key))
				{
					$this->$key = $val;
				}
			}
		}

        //generate the table data
        $this->_generate_data();
	}

	// --------------------------------------------------------------------

	/**
	 * Generate the pagination links
	 *
	 * @access	public
	 * @return	string
	 */
	function create_links()
	{
        // If our item count or per-page total is zero there is no need to continue.
		if ($this->total_rows == 0 OR $this->page_size == 0)
		{
            return '';
		}

        //set up the number of pages
        //if the page size is greater than the number of items, then just return a single page
        if ($this->page_size > $this->total_rows)
        {
            $this->_num_pages = 1;
        }
        else
        {
            $this->_num_pages = ceil($this->total_rows / $this->page_size);
        }


		if ($this->CI->config->item('enable_query_strings') === TRUE OR $this->page_query_string === TRUE)
		{
			if ($this->CI->input->get($this->query_string_segment) != 0)
			{
				$this->cur_page = $this->CI->input->get($this->query_string_segment);

				// Prep the current page - no funny business!
				$this->cur_page = (int) $this->cur_page;
			}
		}
		else
		{
            if ($this->CI->uri->segment($this->uri_segment) != 0)
			{
				$this->cur_page = $this->CI->uri->segment($this->uri_segment);
			}
            else
            {
                $this->cur_page = 1;
            }
            // Prep the current page - no funny business!
            $this->cur_page = (int) $this->cur_page;
		}

        //if there is only 1 page, then current page is the same
        if ($this->_num_pages == 1)
		{
			$this->cur_page = 1;
		}
        else if ($this->page_size <= $this->_num_pages && ($this->page_size * $this->_num_pages) > $this->total_rows)
        {
            $this->cur_page = $this->_num_pages;
        }
        
        //sanity check, cur page cant be less than 1
        //and cant be more than total pages
        $this->cur_page = max($this->cur_page, 1);
        $this->cur_page = min($this->cur_page, $this->_num_pages);

        //number of links shown
		$this->num_links = (int)$this->num_links;
		if ($this->num_links < 1)
		{
			show_error('The number of links must be a positive number.');
		}

        if ($this->_num_pages == 1)
        {
            return '';
        }
        
		// Calculate the start and end numbers. These determine
		// which number to start and end the digit links with
		$start = (($this->cur_page - $this->num_links) > 0) ? $this->cur_page - ($this->num_links - 1) : 1;
		$end   = (($this->cur_page + $this->num_links) < $this->_num_pages) ? $this->cur_page + $this->num_links : $this->_num_pages;

		// Is pagination being used over GET or POST?  If get, add a page_size query
		// string. If post, add a trailing slash to the base URL if needed
		if ($this->CI->config->item('enable_query_strings') === TRUE OR $this->page_query_string === TRUE)
		{
			$this->base_url = rtrim($this->base_url).'&amp;'.$this->query_string_segment.'=';
		}
		else
		{
			$this->base_url = rtrim($this->base_url, '/') .'/';
		}

		// And here we go...
		$output = '';

		// Render the "First" link
		if  ($this->first_link !== FALSE AND $this->cur_page > ($this->num_links + 1))
		{
            $first_url = $this->base_url.'1/'.$this->page_size.'/'.$this->sort_by.'/'.$this->sort_order;
			$output .= $this->first_tag_open.'<a '.$this->anchor_class.'href="'.$first_url.'">'.$this->first_link.'</a>'.$this->first_tag_close;
		}

		// Render the "previous" link
		if  ($this->prev_link !== FALSE AND $this->cur_page != 1)
		{
			$i = $this->cur_page - 1;

			if ($i == 0 && $this->first_url != '')
			{
				$output .= $this->prev_tag_open.'<a '.$this->anchor_class.'href="'.$this->first_url.'">'.$this->prev_link.'</a>'.$this->prev_tag_close;
			}
			else
			{
				$i = ($i == 0) ? '' : $this->prefix.$i.'/'.$this->page_size.$this->suffix.'/'.$this->sort_by.'/'.$this->sort_order;
				$output .= $this->prev_tag_open.'<a '.$this->anchor_class.'href="'.$this->base_url.$i.'">'.$this->prev_link.'</a>'.$this->prev_tag_close;
			}

		}

		// Render the pages
		if ($this->display_pages !== FALSE)
		{
			// Write the digit links
			for ($loop = $start -1; $loop <= $end; $loop++)
			{
				$i = ($loop * $this->page_size) - $this->page_size;

				if ($i >= 0)
				{
					if ($this->cur_page == $loop)
					{
                        $output .= $this->cur_tag_open.$loop.$this->cur_tag_close; // Current page
					}
					else
					{
						$n = ($i == 0) ? '' : $i;
						if ($n == '' && $this->first_url != '')
						{							
                            $output .= $this->num_tag_open.'<a '.$this->anchor_class.'href="'.$this->first_url.'">'.$loop.'</a>'.$this->num_tag_close;                            
                        }
						else
						{
							$n = ($n == '') ? '' : $this->prefix.$n.$this->suffix;
                            $uri = $this->base_url.$loop."/".$this->page_size.'/'.$this->sort_by.'/'.$this->sort_order;
							$output .= $this->num_tag_open.'<a '.$this->anchor_class.'href="'.$uri.'">'.$loop.'</a>'.$this->num_tag_close;
                        }
					}
				}
			}
		}

		// Render the "next" link
		if ($this->next_link !== FALSE AND $this->cur_page < $this->_num_pages)
		{
            $i = $this->prefix.($this->cur_page + 1).'/'.$this->page_size.$this->suffix.'/'.$this->sort_by.'/'.$this->sort_order;
            $output .= $this->next_tag_open.'<a '.$this->anchor_class.'href="'.$this->base_url.$i.'">'.$this->next_link.'</a>'.$this->next_tag_close;
		}

		// Render the "Last" link
		if ($this->last_link !== FALSE AND ($this->cur_page + $this->num_links) < $this->_num_pages)
		{
			$i = $this->prefix.($this->_num_pages).'/'.$this->page_size.$this->suffix.'/'.$this->sort_by.'/'.$this->sort_order;
			$output .= $this->last_tag_open.'<a '.$this->anchor_class.'href="'.$this->base_url.$i.'">'.$this->last_link.'</a>'.$this->last_tag_close;
		}

		// Kill double slashes.  Note: Sometimes we can end up with a double slash
		// in the penultimate link so we'll kill all double slashes.
		$output = preg_replace("#([^:])//+#", "\\1/", $output);

		// Add the wrapper HTML if exists
		return $this->full_tag_open.$output.$this->full_tag_close;

	}

    /**
	 * Generate the table data and headings
	 *
	 * @access	public
	 * @return	string
	 */
    function create_table()
    {
        //check that the correct data is available
        if ( ! is_array($this->table_columns) OR ! is_array($this->_table_records))
        {
            return '';
        }
        
        $this->CI->load->helper('url');
        $this->CI->load->library('table');
        $this->CI->table->set_empty("&nbsp;");

        $headings = $this->_create_headings();
        $this->CI->table->set_heading($headings);

        foreach ($this->_table_records as $record)
        {
            $row = array();
            foreach ($this->table_columns as $column)
            {
                $text = (isset($column['text'])) ? $column['text'] : $record->$column['database'];
                if (isset($column['anchor_link']) AND isset($column['anchor_field']))
                {
                    $link = $column['anchor_link'] . $record->$column['anchor_field'];
                    $extra = (isset($column['extra'])) ? $column['extra'] : array();
                    $row[] = anchor($link, $text, $extra);
                }
                else
                {
                    $row[] = $text;
                }               
            }
            $this->CI->table->add_row($row);
        }
        return $this->CI->table->generate();
    }

    /**
	 * Generate the table headings
	 *
	 * @access	private
	 * @return	array
	 */
    function _create_headings()
    {
        $headings = array();
        foreach ($this->table_columns as $col)
        {
            $link = '';
            $class = '';

            if (isset($col['database']))
            {
                if ($this->sort_by == $col['database'])
                {
                    $class = ($this->sort_order == 'asc') ? array('class' => 'asc') : array('class' => 'desc');
                }                
            }

            //create a sortable column
            if (isset($col['sortable']) && isset($col['database']))
            {
                if ($col['sortable'] == TRUE)
                {
                    $link = $this->base_url . '/';
                    $link .= $this->cur_page . '/' . $this->page_size . '/';
                    $link .= $col['database'] . '/';
                    $link .= ($this->sort_order == 'asc' && $this->sort_by == $col['database'] ) ? 'desc/' : 'asc/';

                    //if the header text has not been set, then use the database field
                    $text = (isset($col['header'])) ? $col['header'] : $col['database'];
                    $header = anchor($link, $text, $class);
                }
            }
            //create a non sortable column
            elseif (isset($col['header']))
            {
                $header = $col['header'];
            }
            $headings[] = $header;
        }
        return $headings;
    }

    /**
	 * Generates an array summary of the current recordset
     *
	 * @access	public
	 * @return	array
	 */
    function create_summary()
    {
        $output = array();

        $output['page']     = $this->cur_page;
        $output['pages']    = $this->_num_pages;
        $output['start']    = ($this->cur_page * $this->page_size) - $this->page_size + 1;
        $output['end']      = min($this->total_rows, $this->cur_page * $this->page_size);
        $output['count']    = $this->total_rows;
        $output['current']  = count($this->_table_records);
        return $output;
    }

    /**
	 * Generate the page size links
	 *
	 * @access	public
	 * @return	string
	 */
    function create_page_sizes()
    {
        $output = '';
        foreach($this->page_sizes as $value)
        {
            //current page
            if ($value == $this->page_size)
            {
                $output .= $this->cur_tag_open.$value.$this->cur_tag_close;
            }
            else
            {
                $uri = $this->base_url.'/'.$this->cur_page."/".$value;
                $output .= $this->num_tag_open.'<a '.$this->anchor_class.'href="'.$uri.'">'.$value.'</a>'.$this->num_tag_close;
            }
        }
        $output = $this->size_tag_open.$output.$this->size_tag_close;
        $output = preg_replace("#([^:])//+#", "\\1/", $output);
        return $output;
    }

    /**
	 * Generates the record count and actual recorset to be used in the table
     * Note that the record_function must be set in your model, with the parameters
     * in this order: $sort_by, $sort_order, $offset, $limit
     *
     *
	 * @access	private
	 * @return	void
	 */
    function _generate_data()
    {
        //get the total number of records
        if (isset($this->model_name) && isset($this->count_function))
        {
            $model = $this->model_name;
            $function = $this->count_function;
            $this->total_rows = $this->CI->$model->$function();           
        }

        $this->page_size = $this->_calculate_page_size();
        $this->page_number = $this->_calculate_page_number();
        $this->sort_by = $this->_calculate_sort_by();
        $this->sort_order = $this->_calculate_sort_order();
        
        $offset = ($this->page_number * $this->page_size) - $this->page_size;

        //get the actual recordset
        if (isset($this->model_name) && isset($this->record_function))
        {
            $model = $this->model_name;
            $record_function = $this->record_function;

            $this->_table_records = $this->CI->$model->$record_function($this->sort_by, $this->sort_order, $offset, $this->page_size);
        }
    }


    /**
	 * Sets the page_size of the data based on the 4th uri segment
     *
	 * The value cannot be less than the minimum element, or greater
     * than the maximum element specified in the page_sizes var
     *
	 * @access	private
	 * @return	int
	 */
    function _calculate_page_size()
    {
        $size = $this->CI->uri->segment(4);
        if ($size > max($this->page_sizes))
        {
            $size = $this->page_size;
        }
        elseif ($size < min($this->page_sizes))
        {
            $size = $this->page_size;
        }
        return $size;
    }

    /**
	 * Sets the page_size of the data based on the 3rd uri segment
     *
	 * This value will default to 1 if the total rows is greater than the page size
     * or if the page size is less than 1
     *
	 * @access	private
	 * @return	int
	 */
    function _calculate_page_number()
    {
        $page = $this->CI->uri->segment(3);
        return (($this->total_rows > $this->page_size) OR ($page < 1) ? 1 : $page);
    }

    /**
	 * Sets the sort order of the data based on the 5th uri segment
     * 
     * A table_columns array must be passed to this class to prevent
     * users from sorting on incorrect/unavailable database columns
	 *
	 * @access	private
	 * @return	string
	 */
    function _calculate_sort_by()
    {
        $sort_options = array();
        foreach($this->table_columns as $column)
        {
            if ($column['sortable'])
            {
                $sort_options[] = $column['database'];
            }
            if (isset($column['default_sort']))
            {
                $default_sort = $column['database'];
            }
        }
        $sort_by = $this->CI->uri->segment(5);
        return (in_array($sort_by, $sort_options)) ? $sort_by : $default_sort;
    }

	/**
	 * Sets the sort order of the data based on the 6th uri segment
	 * Defaults to 'asc'
     *
	 * @access	private
	 * @return	string
	 */
    function _calculate_sort_order()
    {
        $sort_order = $this->CI->uri->segment(6);
        return ($sort_order == 'desc') ? 'desc' : 'asc';
    }



}
// END Paginator Class

/* End of file Paginator.php */
/* Location: ./application/libraries/Paginator.php */